1 module serve;
2 
3 import arsd.cgi;
4 import std.stdio;
5 import std.file;
6 import std.string;
7 import std.process;
8 
9 // -Wl,--export=__heap_base
10 
11 // https://github.com/skoppe/wasm-sourcemaps
12 
13 enum DEFAULT_PATH="build";
14 private __gshared string startPath;
15 
16 string extendedContentTypeFromFileExtension(string thing)
17 {
18 	string ret = contentTypeFromFileExtension(thing);
19 	if(ret != null)
20 		return ret;
21 
22 	if(ret.endsWith(".ogg"))
23 		return "application/ogg";
24 	if(ret.endsWith(".mp4"))
25 		return "application/mp4";
26 
27 	return "application/octet-stream";
28 }
29 
30 import core.thread;
31 import core.sync.mutex;
32 import core.sync.semaphore;
33 void pushWebsocketMessage(string message)
34 {
35 	synchronized
36 	{
37 		foreach(ref conn; connections)
38 			conn.messages~= message;
39 	}
40 	// socket.send(message);
41 	// synchronized websocketMessages~= message;
42 }
43 
44 struct Connection
45 {
46 	size_t id;
47 	string[] messages;
48 }
49 private __gshared size_t id;
50 private __gshared Connection*[] connections;
51 
52 void serveGameFiles(Cgi cgi) 
53 {
54     import std.path;
55     string targetPath = buildNormalizedPath(startPath, DEFAULT_PATH);
56 	string contentType = extendedContentTypeFromFileExtension(cgi.pathInfo);
57 	string file = buildNormalizedPath(targetPath, cgi.pathInfo[1..$]);
58 
59 	if(cgi.websocketRequested())
60 	{
61 		auto socket = cgi.acceptWebsocket();
62 		Connection conn = Connection(id);
63 		synchronized
64 		{
65 			connections~= &conn;
66 			id++;
67 		}
68 		enum __updateMsg = 0xff;
69 		auto msg = socket.recv();
70 		while(msg.opcode != WebSocketOpcode.close) 
71 		{
72 			synchronized
73 			{
74 				foreach(socketMsg; conn.messages)
75 					socket.send(socketMsg);
76 				conn.messages.length = 0;
77 			}
78 			msg = socket.recv();
79 		}
80 
81 		synchronized
82 		{
83 			socket.close();
84 			import std.algorithm;
85 			ptrdiff_t index = countUntil!((Connection* c) => c.id == conn.id)(connections);
86 			if(index != -1)
87 			{
88 				connections = connections[0..index]~ connections[index+1..$];
89 			}
90 		}
91 		writeln("AutoReloading WebSocket[", conn.id, "] closed.");
92 	}
93 	else if(cgi.pathInfo == "/") 
94 	{
95 		string indexHTML = buildNormalizedPath(targetPath, "index.html");
96 		if(exists(indexHTML))
97 		{
98 			import std.string;
99 			string reloadServer = replace(import("reload_server.js"), "$WEBSOCKET_SERVER$", "ws://localhost:"~server.listeningPort.to!string);
100 			cgi.write(readText(indexHTML)~"<script> "~reloadServer~"</script>", true);
101 		}
102 		else
103 		{
104 			// index
105 			string html = "<html><head><title>Hipreme Engine Webassembly Server</title></head><body><ul>";
106 			foreach(string name; dirEntries(targetPath, SpanMode.shallow)) 
107 			{
108 				name = name[targetPath.length..$];
109 				html ~= "<li><a href=\"" ~ name ~ "\">" ~ name ~"</a></li>";
110 			}
111 			html~= "</body></html>";
112 				cgi.write(html, true);
113 
114 		}
115 	}
116 	else if(contentType)
117 	{
118 		if(!exists(file))
119 		{
120 			cgi.setResponseStatus("404 file not found");
121 		}
122 		else
123 		{
124 			cgi.setResponseHeader("Access-Control-Expose-Headers: Content-Length");
125 			cgi.setResponseContentType(contentType);
126 			cgi.setResponseStatus(200);
127 			if(contentType[0.."text".length] == "text")
128 				cgi.write(readText(file), true);
129 			else
130 				cgi.write(read(file), true);
131 			writeln("GET ", file, " 200");
132 		}
133 	}
134 
135 }
136 
137 private RequestServer server;
138 /++
139 	This is the function [GenericMain] calls. View its source for some simple boilerplate you can copy/paste and modify, or you can call it yourself from your `main`.
140 	Params:
141 		fun = Your request handler
142 		CustomCgi = a subclass of Cgi, if you wise to customize it further
143 		maxContentLength = max POST size you want to allow
144 		args = command-line arguments
145 	History:
146 	Documented Sept 26, 2020.
147 +/
148 
149 
150 
151 void hipengineCgiMain(alias fun, CustomCgi = Cgi, long maxContentLength = defaultMaxContentLength)
152 (string[] args, string servePath, shared ushort* port, shared Semaphore sem)  if(is(CustomCgi : Cgi))
153 {
154     startPath = servePath;
155 	if(tryAddonServers(args))
156 		return;
157 	if(trySimulatedRequest!(fun, CustomCgi)(args))
158 		return;
159 
160 	// you can change the port here if you like
161 	*port = 9000;
162 	server.listeningPort = *port;
163 
164 	string host = server.listeningHost;
165 	if(host == "") host = "localhost";
166 
167 
168 	// then call this to let the command line args override your default
169 	server.configureFromCommandLine(args);
170 
171 
172 
173 	// and serve the request(s).
174 
175 	while(*port != 0)
176 	{
177 		try{
178 			server.listeningPort = *port;
179 			writeln("HipremeEngine Dev Server listening from ", host,":",server.listeningPort);
180 			(cast()sem).notify;
181 			server.serve!(fun, CustomCgi, maxContentLength)();
182 			return;
183 		}
184 		catch(Exception e)
185 		{
186 			*port = cast(ushort)(*port + 1);
187 		}
188 	}
189 
190 	writeln("Hipreme Engine Dev server could not start.");
191 }
192 
193 
194 void stopServer()
195 {
196     import core.stdc.stdlib;
197 	pushWebsocketMessage("close");
198     exit(0);
199 }